In [1]:
import numpy as np
%matplotlib inline
import matplotlib.pyplot as plt
from sympy import *
from mpmath import degrees, radians
init_printing()
This derivation is based on the USGS Professional Paper 1395: Map Projections — A Working Manual, ch 25, pp 195 – 199.
In [2]:
radius = Symbol('R')
semimajor, ellipticity = symbols('a e', positive=True)
central_lat, central_lon = symbols('phi_1 lambda_0')
point_lat, point_lon = symbols('phi lambda')
radius, semimajor, ellipticity, \
central_lat, central_lon, \
point_lat, point_lon
Out[2]:
In [3]:
def subdict(eq, vals):
for k, v in vals.items():
eq = eq.subs(k, v)
return eq
In [4]:
# Eq. 5-3
c = acos(sin(central_lat) * sin(point_lat) +
cos(central_lat) * cos(point_lat) * cos(central_lon - point_lon))
c
Out[4]:
In [5]:
# Eq. 25-2
kp = c / sin(c)
kp
Out[5]:
In [6]:
xs = radius * kp * cos(point_lat) * sin(point_lon - central_lon)
xs
Out[6]:
In [7]:
ys = radius * kp * (cos(central_lat) * sin(point_lat) -
sin(central_lat) * cos(point_lat) * cos(point_lon - central_lon))
ys
Out[7]:
Unfortunately, the paper does not state full exact equations for the ellipsoid, but only approximations. I don't quite know how to write them in SymPy yet.
The equation for $M$ is given as Eq. 3-21 on pg 197:
\begin{align} M = a [ & (1 - e^2 / 4 - 3e^4 / 64 - 5e^6 / 256 - \dots) \phi \\ - & (3e^2 / 8 + 3e^4 / 32 + 45e^6 / 1024 + \dots) \sin(2 \phi) \\ + & (15e^4 / 256 + 45e^6 / 1024 + \dots) \sin(4 \phi) \\ - & (35e^6 / 3072 + ...) \sin(6 \phi) \\ + & \dots ] \end{align}The numerator in the first term can be written easily as: $$ \sum_{n=1, 2, \dots} (2n-1) e^{2n} $$ but the denominator is: $$ 4, 64, 256, \dots = 2^2, 2^6, 2^8, \dots $$ which doesn't seem to follow a sane pattern, and the second, third, fourth, etc. terms are worse. Possibly, some trig rules are reducing things, but the pattern is not particularly evident the way they've written it. I also haven't yet read chapter 3, so perhaps this is all explained there.
Let's try out the reduced form anyway, assuming they kept only the relevant terms...
In [8]:
M = semimajor * (
(1 - ellipticity**2 / 4 - 3 * ellipticity**4 / 64 - 5 * ellipticity**6 / 256) * point_lat -
(3 * ellipticity**2 / 8 + 3 * ellipticity**4 / 32 + 45 * ellipticity**6 / 1024) * sin(2 * point_lat) +
(15 * ellipticity**4 / 256 + 45 * ellipticity**6 / 1024) * sin(4 * point_lat) -
(35 * ellipticity**6 / 3072) * sin(6 * point_lat)
)
M
Out[8]:
In [9]:
Mp_np = M.subs(point_lat, radians(90))
Mp_np
Out[9]:
In [10]:
rho_np = Mp_np - M
rho_np
Out[10]:
In [11]:
xnp = rho_np * sin(point_lon - central_lon)
ynp = -rho_np * cos(point_lon - central_lon)
In [12]:
Mp_sp = M.subs(point_lat, radians(-90))
Mp_sp
Out[12]:
In [13]:
rho_sp = Mp_sp + M
rho_sp
Out[13]:
In [14]:
xsp = xnp
ysp = rho_sp * cos(point_lon - central_lon)
In [15]:
M_guam = M
M1_guam = M_guam.subs(point_lat, central_lat)
M1_guam
Out[15]:
In [16]:
x = (semimajor * (point_lon - central_lon) * cos(point_lat) /
(1 - ellipticity**2 * sin(point_lat)**2)**0.5)
y = (M_guam - M1_guam + x**2 * tan(point_lat) *
(1 - ellipticity**2 * sin(point_lat)**2)**0.5 / (2 * semimajor))
In [17]:
# Default Globe in Cartopy
WGS84_semimajor = Rational(6378137)
WGS84_flattening = Rational(1, 298.257223563)
# Plotting fails if I leave this bit as exact, thinking there are complex values,
# even though the input to sqrt is positive, and it's supposed to return the positive root.
WGS84_ellipticity = sqrt(2 * WGS84_flattening - WGS84_flattening**2).evalf()
# Default parameters in Cartopy
lat0_default = radians(0)
lon0_default = radians(0)
In [18]:
# From numerical examples, pg 337
radius_example = 3.0
lat0_example = 40
lon0_example = -100
sphere_vars = {radius: radius_example,
central_lat: radians(lat0_example),
central_lon: radians(lon0_example)}
I chose the following boundary locations since they work for other projections, but I'm not certain they'll work here.
In [19]:
lon_min_sphere = radians(lon0_example - 180)
lon_max_sphere = radians(lon0_example + 180)
lat_min_sphere = radians(lat0_example - 90)
lat_max_sphere = radians(lat0_example + 90)
example_xs = subdict(xs, sphere_vars)
eq1x = example_xs.subs(point_lat, lat_max_sphere)
eq2x = example_xs.subs(point_lon, lon_max_sphere)
eq3x = example_xs.subs(point_lat, lat_min_sphere)
eq4x = example_xs.subs(point_lon, lon_min_sphere)
example_ys = subdict(ys, sphere_vars)
eq1y = example_ys.subs(point_lat, lat_max_sphere)
eq2y = example_ys.subs(point_lon, lon_max_sphere)
eq3y = example_ys.subs(point_lat, lat_min_sphere)
eq4y = example_ys.subs(point_lon, lon_min_sphere)
plotting.plot_parametric((eq1x, eq1y, (point_lon, lon_min_sphere, lon_max_sphere)),
(eq2x, eq2y, (point_lat, lat_max_sphere, lat_min_sphere)),
(eq3x, eq3y, (point_lon, lon_max_sphere, lon_min_sphere)),
(eq4x, eq4y, (point_lat, lat_min_sphere, lat_max_sphere)),
title='Guessed Sphere Boundaries')
Out[19]:
In [20]:
lat_magnitude = subdict((point_lat - central_lat) / radians(90), sphere_vars)
lon_magnitude = subdict((point_lon - central_lon) / radians(180), sphere_vars)
plotting.plot3d_parametric_surface(example_xs, example_ys, lat_magnitude,
(point_lat, lat_min_sphere, lat_max_sphere),
(point_lon, lon_min_sphere, lon_max_sphere),
xlabel='X', ylabel='Y', title='Sphere - Latitude Dependence')
plotting.plot3d_parametric_surface(example_xs, example_ys, lon_magnitude,
(point_lat, lat_min_sphere, lat_max_sphere),
(point_lon, lon_min_sphere, lon_max_sphere),
xlabel='X', ylabel='Y', title='Sphere - Longitude Dependence')
Out[20]:
The longitude and latitude dependence is pretty complicated (worse than the above seems to show). Maybe we can find the boundaries using the derivative, but I'm not sure.
In [21]:
xs_deriv_lat = diff(xs, point_lat)
xs_deriv_lon = diff(xs, point_lon)
ys_deriv_lat = diff(ys, point_lat)
ys_deriv_lon = diff(ys, point_lon)
In [22]:
xsd_lat = subdict(xs_deriv_lat, sphere_vars)
xsd_lon = subdict(xs_deriv_lon, sphere_vars)
ysd_lat = subdict(ys_deriv_lat, sphere_vars)
ysd_lon = subdict(ys_deriv_lon, sphere_vars)
In [23]:
# Solving them together uses 14+ GiB and still doesn't complete!
# solve((xsd_lat, xsd_lon), (point_lat, point_lon))
# extreme_lat = solve(xsd_lat.subs(point_lon, 0), point_lat)
The above solve does not complete in either manner. So I'm going to bench this attempt for the time being.
In [24]:
ellipsoid_vars = {semimajor: WGS84_semimajor,
ellipticity: WGS84_ellipticity,
central_lat: radians(lat0_default),
central_lon: radians(lon0_default)}
In [25]:
lat0_np = 90
lon0_np = 0
np_vars = {semimajor: WGS84_semimajor,
ellipticity: WGS84_ellipticity,
central_lat: radians(lat0_np),
central_lon: radians(lon0_np)}
lon_min_np = radians(lon0_np - 180)
lon_max_np = radians(lon0_np + 180)
lat_min_np = radians(lat0_np - 180)
lat_max_np = radians(lat0_np)
default_xnp = subdict(xnp, np_vars)
default_ynp = subdict(ynp, np_vars)
lat_magnitude = subdict(point_lat / radians(90), np_vars)
lon_magnitude = subdict(point_lon / radians(180), np_vars)
plotting.plot3d_parametric_surface(default_xnp, default_ynp, lat_magnitude,
(point_lat, lat_min_np, lat_max_np),
(point_lon, lon_min_np, lon_max_np),
xlabel='X', ylabel='Y',
title='Ellipsoid - North Polar Aspect - Latitude Dependence')
plotting.plot3d_parametric_surface(default_xnp, default_ynp, lon_magnitude,
(point_lat, lat_min_np, lat_max_np),
(point_lon, lon_min_np, lon_max_np),
xlabel='X', ylabel='Y',
title='Ellipsoid - North Polar Aspect - Longitude Dependence')
Out[25]:
From the above, it appears the boundary occurs on the minimum latitude, at least for the North Polar Aspect.
In [26]:
lat0_sp = -90
lon0_sp = 0
sp_vars = {semimajor: WGS84_semimajor,
ellipticity: WGS84_ellipticity,
central_lat: radians(lat0_sp),
central_lon: radians(lon0_sp)}
lon_min_sp = radians(lon0_sp - 180)
lon_max_sp = radians(lon0_sp + 180)
lat_min_sp = radians(lat0_sp)
lat_max_sp = radians(lat0_sp + 180)
default_xsp = subdict(xsp, sp_vars)
default_ysp = subdict(ysp, sp_vars)
lat_magnitude = subdict(point_lat / radians(90), sp_vars)
lon_magnitude = subdict(point_lon / radians(180), sp_vars)
plotting.plot3d_parametric_surface(default_xsp, default_ysp, lat_magnitude,
(point_lat, lat_min_sp, lat_max_sp),
(point_lon, lon_min_sp, lon_max_sp),
xlabel='X', ylabel='Y',
title='Ellipsoid - South Polar Aspect - Latitude Dependence')
plotting.plot3d_parametric_surface(default_xsp, default_ysp, lon_magnitude,
(point_lat, lat_min_sp, lat_max_sp),
(point_lon, lon_min_sp, lon_max_sp),
xlabel='X', ylabel='Y',
title='Ellipsoid - South Polar Aspect - Longitude Dependence')
Out[26]:
Again, the boundary occurs at the minimum latitude.
After some thought, it seems that the boundary should be at the greatest distance away from the central point. This makes sense given the properties of this projection. The projection plots azimuth after all. This even explains the "strangeness" for the spherical case; because the central point is not at the poles, the radial distance must be greater than zero. Just blindly taking negative and positive latitudes and longitudes will not cover the whole azimuth, and sometimes produces odd wraparound.
But this actually is good news! It means the boundary is simply half a circumference. Obviously, the meaning is slightly different for an ellipsoid, but it's certainly a lot easier to calculate.